package com.qdd.freight.handler;

import com.qdd.freight.constant.ModeConstants;
import com.qdd.freight.entity.Datas;
import com.qdd.freight.exception.*;
import com.qdd.freight.mapper.*;
import com.qdd.freight.pojo.*;
import com.qdd.freight.util.DateUtils;
import com.qdd.freight.util.ReflectUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import tk.mybatis.mapper.entity.Example;

import java.lang.reflect.Field;
import java.text.DecimalFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 根据运价的组成不同，实现不同的方法，
 * 最终按照统一的计算规则进行总价计算
 *
 * @ClassName AbstractHandler
 * @Description TODO
 * @Author yanpeihao
 * @Date 2019/12/6 21:26
 * @Version 1.0
 **/

public abstract class AbstractHandler {

    @Autowired
    private GoodsShipMapper goodsShipMapper;

    @Autowired
    private YfModeMapper yfModeMapper;

    @Autowired
    private LogisticsMapper logisticsMapper;

    @Autowired
    private DeliveryMapper deliveryMapper;

    @Autowired
    private ForbiddenRegionMapper forbiddenRegionMapper;

    abstract protected String getShipFieldNameFromInstance() throws NoShipFieldNameException;

    /**
     * 获取可用的计算参数
     *
     * @param ship      入参其实现类,快递或物流对象
     * @param shipId    快递/物流id
     * @param consignee 发货-到货地址
     * @return
     * @throws NullAvailableParamsException
     */
    abstract protected Map<String, Object> getAvailableParams(Ship ship, Integer shipId, String consignee) throws NullAvailableParamsException;

    /**
     * 运价计算抽象方法---由具体实现类实现内部计算逻辑
     *
     * @param modeId       快运类型
     * @param goodsId      商品id
     * @param entId        仓库id
     * @param quantity     采购数量
     * @param company      企业编号
     * @param consignee    省市区编号
     * @param boxVolume    整箱体积
     * @param singleVolume 单件体积
     * @param boxWeight    整箱重量
     * @param singleWeight 单件重量
     * @param QTYRate      箱规
     * @return
     * @throws Exception
     */
    abstract public Datas realHandle(GoodsShip goodsShip, Integer shipId, Integer modeId, Integer goodsId,
                                     Integer entId, Integer quantity, Integer company, String consignee,
                                     Double boxVolume, Double singleVolume, Double boxWeight, Double singleWeight,
                                     Integer QTYRate) throws Exception;

    /**
     * 被service调用的主逻辑方法
     * 通用计算框架，controller层会调用该方法执行判断
     *
     * @param modeId       快运类型
     * @param goodsId      商品id
     * @param entId        仓库id
     * @param quantity     采购数量
     * @param company      企业编号
     * @param consignee    省市区编号
     * @param boxVolume    整箱体积
     * @param singleVolume 单件体积
     * @param boxWeight    整箱重量
     * @param singleWeight 单件重量
     * @param QTYRate      箱规
     * @return
     * @throws Exception
     */

    public Datas handelFreight(Integer modeId, Integer goodsId, Integer entId, Integer quantity,
                               Integer company, String consignee, Double boxVolume, Double singleVolume,
                               Double boxWeight, Double singleWeight, Integer QTYRate) throws Exception {
        //1.调用实现类的具体实现，获取对应的goodsShip的字段名
        String fieldName = getShipFieldNameFromInstance();
        if (StringUtils.isEmpty(fieldName)) {
            throw new FreightCountFailureException("未获取到指定的物流承运商！");
        }
        //2.获得goodsShip对象
        GoodsShip goodsShip = getAppointedGoodsShip(goodsId, entId, quantity);
        //3.获取所有值不为0的goodsShip对象的快递/物流id
        Integer shipId = (Integer) ReflectUtil.getValue(goodsShip, fieldName);
        //解析收件地址
        String[] receiverArr = consignee.split("-");
        String receiverAddr = receiverArr[1];
        //调用子类的实现逻辑进行禁运判断，时间和地区，禁运则退出计算
        try {
            //true为不可用的，退出计算
            if (isDisabled(shipId, receiverAddr)) {
                throw new FreightCountFailureException("无可用的计算数据!");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new FreightCountFailureException("计算错误!", e.getCause());
        }

        //调用具体的计算处理器进行结果计算
        Datas datas = realHandle(goodsShip, shipId, modeId, goodsId, entId, quantity, company, consignee, boxVolume,
                singleVolume, boxWeight, singleWeight, QTYRate);
        return datas;
    }

    //================================================================================


    /**
     * 实现方法1：启用
     * 子类可继承的工具方法--确定要使用的goodsShip对象
     * 无阈值，1条数据
     * 有阈值，2条数据，取出快递/物流id的list集合
     *
     * @param goodsId  商品id
     * @param entId    仓库id
     * @param quantity 采购数量
     * @return
     */
    protected GoodsShip getAppointedGoodsShip(Integer goodsId, Integer entId,
                                              Integer quantity) throws FreightCountFailureException {
        //获取该商品的goodsShip对象，如果设置了阈值会有两条记录
        List<GoodsShip> goodsShipList = getGoodsShipListFromDb(goodsId, entId);
        //判断list的长度，1表示没有阈值，2表示设有阈值
        //长度为1，说明没有阈值,获取仅有的一行数据
        if (goodsShipList.size() == 1) {
            return goodsShipList.get(0);
        }
        //如果大于1，比较采购数量和阈值，确定要使用的goodsShip对象
        //1.比较采购数量和阈值确定symbol值， 1为<=weight,2为>weight
        int symbol = goodsShipList.stream()
                .allMatch(item -> item.getWeight().compareTo(quantity) > 0) ? 1 : 2;
        GoodsShip goodsShip = goodsShipList.stream()
                //找出symbol值匹配的对象
                .filter(item -> item.getSymbol() == symbol).findFirst().get();
        //2.根据比较结果获取要使用的快运对象关联的快递/物流编号
        return goodsShip;
    }

//================================================================================
//
//    /*
//     * 实现方法1：未启用
//     * 内部工具方法--确定要使用的goodsShip对象的快递/物流id集合
//     *
//     * @param goodsId  商品id
//     * @param entId    仓库id
//     * @param quantity 采购数量
//     * @return
//     */
////    protected List<Integer> getAppointedShipIds(Integer goodsId, Integer entId,
////                                                Integer quantity) throws FreightCountFailureException {
////        return getAppointedShipIds(getAppointedGoodsShip(goodsId, entId, quantity));
////    }
//
//    /*
//     * 未启用
//     * 内部工具方法---反射获取goodsShip对象关联的,不为0的快递/物流id集合
//     * @param goodsShip 商品承运信息对象
//     * @return
//     */
////    private List<Integer> getAppointedShipIds(GoodsShip goodsShip) {
////        //1.反射获取所有的字段
////        Field[] fields = goodsShip.getClass().getDeclaredFields();
////        //2.找出和运输方式有关的shipId的集合
////        List<Integer> modeIdList = Arrays.asList(fields).stream()
////                //根据属性名找出包含指定子字符、和计价体系有关的字段field
////                .filter(item -> item.getName().contains("shipId")
////                        || item.getName().contains("wlId")
////                        || item.getName().contains("transportId"))
////                .map(item -> (Integer) ReflectUtil.getValue(goodsShip, item.getName()))
////                .filter(item -> item != 0)
////                .peek(item -> System.out.print(item + ","))
////                .collect(Collectors.toList());
////        return modeIdList;
////    }
//
////================================================================================
//
//    /* 未启用
//     * 根据modeId+常量+goodsShip，通过反射获取指定的物流/快递id，
//     * 如果后期发现效率不高，切换到备用方法2--通过数据库获取
//     * 可继承的工具方法---获取计算要使用的快递/物流id
//     *
//     * @param goodsShip
//     * @param fieldName
//     * @return
//     */
////    protected Integer getAppointedShipId(GoodsShip goodsShip, String fieldName) {
////        /*
////            反射获取指定字段的值=goodsShip的所有字段,并找到指定字段名的字段值
////         */
////        //反射获取所有字段
////        Field[] goodsShipFieldList = goodsShip.getClass().getDeclaredFields();
////        //获取goodsShip的指定字段值并返回
////        Integer shipId = Arrays.asList(goodsShipFieldList).stream()
////                .map(item -> (Integer) ReflectUtil.getValue(goodsShip, fieldName))
////                .findAny().get();
////        return shipId;
////    }

    //================================================================================

    /**
     * 内部工具方法---从数据库中获取商品对应的商品信息快运方式对象列表，避免重复查询数据库
     * 如果设置了阈值，会出现两条记录
     *
     * @param goodsId 商品id
     * @param entId   仓库id
     * @return
     */
    private List<GoodsShip> getGoodsShipListFromDb(Integer goodsId, Integer entId) throws FreightCountFailureException {
        Example example = new Example(GoodsShip.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("goodsId", goodsId)
                .andEqualTo("entId", entId);
        List<GoodsShip> goodsShipList = goodsShipMapper.selectByExample(example);
        if (goodsShipList == null || goodsShipList.size() == 0) {
            throw new FreightCountFailureException("该仓库下未查询到符合条件的货运列表！");
        }
        return goodsShipList;
    }

//================================================================================

    /**
     * 方法1：启用
     * 可继承的方法---反射获取指定商品、仓库、数量下的goodsShip对象的快递/物流相关的，值不为0的字段
     *
     * @param goodsId
     * @param entId
     * @param quantity
     * @return
     */
    protected List<Field> getAppointedGoodsShipField(Integer goodsId, Integer entId,
                                                     Integer quantity) throws FreightCountFailureException {
        //1.调用公共方法从数据库中获取商品快运对象
        GoodsShip goodsShip = getAppointedGoodsShip(goodsId, entId, quantity);
        //2.反射获取goodsShip所有的字段
        Field[] fields = goodsShip.getClass().getDeclaredFields();
        //3.找出和运输方式有关的字段的集合
        List<Field> fieldList = Arrays.asList(fields).stream()
                //根据属性名找出包含指定子字符、和计价体系有关的字段field
                .filter(item -> item.getName().contains("shipId")
                        || item.getName().contains("wlId")
                        || item.getName().contains("transportId"))
                .filter(item -> (Integer) ReflectUtil.getValue(goodsShip, item.getName()) != 0)
//                .peek(item -> System.out.print(item + ","))
                .collect(Collectors.toList());
        return fieldList;
    }

//================================================================================

    /**
     * 实现父类接口方法，编写自有逻辑
     * 获取商品在指定仓库中所支持快运方式编号yf_mode
     *
     * @param goodsId  商品id
     * @param entId    仓库id
     * @param quantity 采购数量
     * @return
     */
    protected List<YfMode> getModeList(Integer goodsId, Integer entId, Integer quantity) throws FreightCountFailureException {
        //获取所有的modeId
        List<Integer> modeIdList = getModeIdList(goodsId, entId, quantity);
        //根据快运方式的modeId从数据库fcs_mode，获取对应快运方式的name --查询数据库
        Example example = new Example(YfMode.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andIn("yfMode", modeIdList);
        List<YfMode> modes = yfModeMapper.selectByExample(example);
        return modes;
    }

    /**
     * 获取该商品关联的快递/物流的快运方式modeId列表
     *
     * @param goodsId  商品id
     * @param entId    仓库id
     * @param quantity 采购数量
     * @return
     * @throws FreightCountFailureException
     */
    private List<Integer> getModeIdList(Integer goodsId, Integer entId, Integer quantity) throws FreightCountFailureException {
        //1.创建常量类对象
        ModeConstants modeConstants = new ModeConstants();
        //2.获取所有的不为0的快递/物流字段
        List<Field> goodsShipField = getAppointedGoodsShipField(goodsId, entId, quantity);
        //3.获取所有的goodsShip的字段名---字段的值不为0,转换为大写以匹配常量类中的字段名
        List<String> fieldList = goodsShipField.stream().map(item -> item.getName().toUpperCase())
                .collect(Collectors.toList());
        /*
            4.从常量中找出字段名和上述字段列表匹配的字段的值
         */
        //4.1获取常量类的所有字段
        Field[] ConsFields = modeConstants.getClass().getDeclaredFields();
        //4.2找出和运输方式有关的字段的集合
        return Arrays.stream(ConsFields)
                //根据shipField列表中包含的字段名匹配的field字段，并获取字段值
                .filter(item -> fieldList.contains(item.getName()))
                .map(item -> (Integer) ReflectUtil.getValue(modeConstants, item.getName()))
                .peek(item -> System.out.println("获取到的ModeId:" + item))
                .collect(Collectors.toList());
    }
//========================================================================================================

    /**
     * 公共方法----封装返回结果数据
     *
     * @param modeId   快运类型
     * @param goodsId  商品id
     * @param entId    仓库id
     * @param quantity 采购数量
     * @param freight  运费价格
     * @return
     * @throws NoGoosShipFoundException
     */
    protected Datas packageDatas(Integer modeId, Integer goodsId, Integer entId, Integer quantity, Double freight)
            throws FreightCountFailureException, FreightToBeZeroExcption, NoSuchElementException {
    /*
        3.封装结果返回
     */
        if (0 == freight) {
            throw new FreightToBeZeroExcption("运费计算结果为0！");
        }
        Datas datas = new Datas();
        DecimalFormat df = new DecimalFormat("#.00");
        //获得所有支持的modes对象列表
        List<YfMode> modeList = getModeList(goodsId, entId, quantity);
        //将所有mode的数据封装到peisong数组
        List<Map<String, Object>> peiSong = packagePeiSong(modeList, modeId);
        //将被选中的mode对象的数据封装到peisong数组中
        Map<String, Object> selectedModeMap = modeList.stream().filter(item -> modeId.equals(item.getYfMode()))
                //调用方法封装被选中的mode对象
                .map(item -> packageSelectedMode(item, Double.valueOf(df.format(freight))))
                .findAny().get();
        peiSong.add(selectedModeMap);
        //设置Yf_mode
        datas.setYf_mode(modeId);
        //设置peisong数组
        datas.setPeisong(peiSong);
        return datas;
    }

    /**
     * 封装被选中的mode对象的数据为map集合
     *
     * @param mode
     * @param freight
     * @return
     */
    private Map<String, Object> packageSelectedMode(YfMode mode, Double freight) {
        HashMap<String, Object> modeMap = new HashMap<>();
        modeMap.put("name", mode.getName());
        modeMap.put("price", freight);
        modeMap.put("yf_mode", mode.getYfMode());
        return modeMap;
    }

//========================================================================================================

    /**
     * 1.计算处理器调用此方法封装除被选中的mode对象之外的数据
     * 2.查询处理器调用此方法封装全部数据
     *
     * @param modes  当前商品在当前仓库下所支持的承运方式modeId清单
     * @param modeId 当前被选中的承运方式的id
     * @return
     */
    protected List<Map<String, Object>> packagePeiSong(List<YfMode> modes, Integer modeId) {
        //定义集合，用于存储多个mode对象的数据
        List<Map<String, Object>> peisong = new ArrayList<>();
        //将获取到的数据重新封装到map中,排除被选中的mode数据--单独再封装
        modes.forEach(mode -> {
            if (modeId != mode.getYfMode()) {
                HashMap<String, Object> modeMap = new HashMap<>();
                modeMap.put("name", mode.getName());
                modeMap.put("price", "0.00");
                modeMap.put("yf_mode", mode.getYfMode());
                //多个数据存入list集合
                peisong.add(modeMap);
            }
        });
        return peisong;
    }
//========================================================================================================

    /**
     * 物流承运商和禁运列表获取
     * 内部方法---根据计价方式（快递/物流的编号）判断是否禁运的公共方法
     * 由具体子类实现，在抽象类中根据modeId判断快递的类型比较复杂，且扩展性收到影响。
     *
     * @param shipId       计价方式id
     * @param receiverAddr 到货地址字符串
     * @return
     */
    private boolean isDisabled(Integer shipId, String receiverAddr) throws Exception {
        //1.调用公共方法从数据库获取物流对象,未查询到对应的ship对象抛出异常
        Ship ship = getLogistics(shipId);
        //2.查询禁运表,获取禁运地址清单
        List<String> forbiddenRegionList;
        //如果不是物流,则尝试获取快递对象
        if (ship == null) {
            ship = getDelivery(shipId);
            forbiddenRegionList = forbiddenRegionMapper.getDelForAddrList(shipId);
        } else {
            forbiddenRegionList = forbiddenRegionMapper.getLogForAddrList(shipId);
        }
        //如果都不存在则退出计算
        if (ship == null) {
            throw new NoShipFoundException("未获取到指定的物流承运商！");
        }
        //5.返回false表示该物流对象是可用的
        if (forbiddenRegionList == null || forbiddenRegionList.size() == 0) {
            return false;
        }
        //3.调用父类方法判断是否可用,true为不可用,false为可用
        return forbiddenJudge(ship, receiverAddr, forbiddenRegionList);
    }
//========================================================================================================


    /**
     * 禁运判断
     *
     * @param ship                对象,真正入参的是其子类对象物流或快递对象
     * @param receiverAddr        到货地址字符串
     * @param forbiddenRegionList 从数据库获取的禁运地区清单
     * @return true 为禁运中
     * @throws Exception
     */

    private static boolean forbiddenJudge(Ship ship, String receiverAddr, List<String> forbiddenRegionList) throws Exception {

        //2.该物流的有效期 + 启用状态判断，提示该地区不支持此承运方式
        //判断启用状态
        if (ship.getEnabled() == 0) {
            throw new FreightCountFailureException("该物流承运商未启用！");
        }
        //3.判断是否设置了有效期的值：如果为0，则说明长期有效；不为0，判断当时时间是否在有效期内
        //有有效期设置，且已经超出有效内，提示该地区不支持此承运方式
        if (ship.getTimeValidity() != 0 && !(DateUtils.judgeValidity(new Date(), ship.getAddTime(), ship.getTimeValidity()))) {
            throw new FreightCountFailureException(" 该快递已经超出有效期,不支持此承运方式 ！");
        }

        // 4.判断是否该物流/快递对象存在禁运标志，返回false表示该物流对象是可用的
        if (ship.getForbiddenStatus() == 0) {
            return false;
        }
        //6.如果有禁运设置，调用方法判断是否在禁运字符串内,true为禁运,false表明当前物流对象可用
        return forbiddenRegionList.stream().anyMatch(receiverAddr::startsWith);
    }
//========================================================================================================

    /**
     * 根据shipId获取物流对象
     *
     * @param shipId 物流/快递id
     * @return
     */
    protected Logistics getLogistics(Integer shipId) {
        return logisticsMapper.selectByPrimaryKey(shipId);
    }

    /**
     * 根据shipId获取快递对象
     *
     * @param shipId 物流/快递id
     * @return
     */
    protected Delivery getDelivery(Integer shipId) {
        return deliveryMapper.selectByPrimaryKey(shipId);
    }

}
